function TestCase( name ) {

	this.tracker		= false;
	this.name 			= name;

	this.setTracker = function( tracker ) {
		this.tracker = tracker;
	}
	
	this.setup = function() {
	}
	
	this.teardown = function(){
	}

	// TODO: Get this working...
	this._checkValidToRun = function( action ) {
		if ( ! this.tracker ) {
//			raiseAnExceptionInHereWhereTheTextIs;
			alert( 'Cannot ' + action + ' unless it is part of a suite, and therefore has a tracker' );
		}
	}
	
	this._assert = function( expectation ) {

		this._checkValidToRun( 'issue an assertion against a testcase' );

		if ( expectation.evaluate() ) {
			this.tracker.reportAssertionPass( expectation );
		} else {
			this.tracker.reportAssertionFail( expectation );
		}
	}
	
	this.assertTrue = function( actual, message ) {
		var expectation = new ExpectTrue( actual, message );
		this._assert( expectation );
	}
	
	this.assertFalse = function( actual, message ) {
		var expectation = new ExpectFalse( actual, message );
		this._assert( expectation );
	}
	
	this.assertEqual = function( expected, actual, message ) {
		var expectation = new ExpectEqual( expected, actual, message );
		this._assert( expectation );
	}
	
	this.run = function() {

		this._checkValidToRun( 'run a testcase' );
		this.tracker.reportTestCaseStart( this.name );
		this._runTests();
		this.tracker.reportTestCaseEnd( this.name );
	}
	
	this.isATestMethod = function( thisMemberThing ) {
		return ( hasMethod( this, thisMemberThing ) && /^test_/.test( thisMemberThing ) );
	}
	
	// Can this be made more generic, expectationised and then made public?
	this._executeAndAssertDoesNotThrowAnException = function( method ) {
		try {
			this[method]();
		}
		catch( exception ) {
			this.assertTrue( false, method +' should not throw an exception: ' + exception.name + ' - ' + exception.message );
		}
	}
	
	this._runTestMethod = function( testMethod ) {
	
		this.tracker.reportTestMethodStart( testMethod );
	
		this._executeAndAssertDoesNotThrowAnException( 'setup' );
		this._executeAndAssertDoesNotThrowAnException( testMethod );
		this._executeAndAssertDoesNotThrowAnException( 'teardown' );
	
		this.tracker.reportTestMethodEnd( testMethod );
	}
	
	this._runTests = function() {
		for( thisMemberThing in this ) {
			if ( this.isATestMethod( thisMemberThing ) ) {
				this._runTestMethod( thisMemberThing );
			}
		}
	}
}




function TestSuite() {

	this.testCases 			= new Array();
	this.tracker			= new TestResultTracker();

	this.addInterestedParty = function( party ) {
		this.tracker.addInterestedParty( party );
	}

	this.addTestCase = function( testCase ) {
		testCase.setTracker( this.tracker );
		this.testCases[ this.testCases.length ] = testCase;
	}
	
	this.run = function() {
		this.tracker.reportTestSuiteStart();
		for( thisTestCase in this.testCases ) {
			this.testCases[ thisTestCase ].run();
		}
		this.tracker.reportTestSuiteEnd();
	}
}




function Notifier( tracker ) {

	this.interestedParties  = new Array();

	this.addInterestedParty = function ( interestedParty ) {
		this.interestedParties[ this.interestedParties.length ] = interestedParty;
	}
	
	this.notifyAssertionPass = function( expectation ) {
		for ( thisPartyKey in this.interestedParties ) {
			this.interestedParties[ thisPartyKey ].paintAssertionPass( expectation );
		}
	}
	
	this.notifyAssertionFail = function( expectation ) {
		for ( thisPartyKey in this.interestedParties ) {
			this.interestedParties[ thisPartyKey ].paintAssertionFail( expectation );
		}
	}

	this.notifyTestSuiteStart = function() {
		for ( thisPartyKey in this.interestedParties ) {
			this.interestedParties[ thisPartyKey ].paintTestSuiteStart();
		}
	}

	this.notifyTestSuiteEnd = function() {
		for ( thisPartyKey in this.interestedParties ) {
			this.interestedParties[ thisPartyKey ].paintTestSuiteEnd();
		}
	}
	
	this.notifyTestCaseStart = function() {
		for ( thisPartyKey in this.interestedParties ) {
			this.interestedParties[ thisPartyKey ].paintTestCaseStart();
		}
	}

	this.notifyTestCaseEnd = function() {
		for ( thisPartyKey in this.interestedParties ) {
			this.interestedParties[ thisPartyKey ].paintTestCaseEnd();
		}
	}
	
	this.notifyTestMethodStart = function() {
		for ( thisPartyKey in this.interestedParties ) {
			this.interestedParties[ thisPartyKey ].paintTestMethodStart();
		}
	}

	this.notifyTestMethodEnd = function() {
		for ( thisPartyKey in this.interestedParties ) {
			this.interestedParties[ thisPartyKey ].paintTestMethodEnd();
		}
	}
}




function TestResultTracker() {

	this.currentTestCase					= '';
	this.currentTestMethod					= '';

	this.thisTestCasePassed					= true;
	this.thisTestMethodPassed				= true;
	
	this.totalTestCasesPassed				= 0;
	this.totalTestCasesFailed				= 0;

	this.totalTestMethodsPassed				= 0;
	this.totalTestMethodsFailed				= 0;

	this.totalAssertionsPassed				= 0;
	this.totalAssertionsFailed				= 0;
	
	this.thisTestCasesAssertionsPassed		= 0;
	this.thisTestCasesAssertionsFailed		= 0;
	
	this.thisTestMethodsAssertionsPassed	= 0;
	this.thisTestMethodsAssertionsFailed	= 0;
	
	this.notifier = new Notifier();

	this.getTotalNumberOfTestCasesPassed = function() {
		return this.totalTestCasesPassed;
	}

	this.getTotalNumberOfTestCasesFailed = function() {
		return this.totalTestCasesFailed;
	}
	
	this.getTotalNumberOfTestCases = function() {
		return this.getTotalNumberOfTestCasesFailed() + this.getTotalNumberOfTestCasesPassed();
	}

	this.getTotalNumberOfAssertionsPassed = function() {
		return this.totalAssertionsPassed;
	}

	this.getTotalNumberOfAssertionsFailed = function() {
		return this.totalAssertionsFailed;
	}

	this.getTotalNumberOfAssertions = function() {
		return this.getTotalNumberOfAssertionsFailed() + this.getTotalNumberOfAssertionsPassed();
	}

	this.getThisTestCasesNumberOfAssertionsPassed = function() {
		return this.thisTestCasesAssertionsPassed;
	}

	this.getThisTestCasesNumberOfAssertionsFailed = function() {
		return this.thisTestCasesAssertionsFailed;
	}

	this.getThisTestCasesNumberOfAssertions = function() {
		return this.getThisTestCasesNumberOfAssertionsFailed() + this.getThisTestCasesNumberOfAssertionsPassed();
	}

	this.getThisTestMethodsNumberOfAssertionsPassed = function() {
		return this.thisTestMethodsAssertionsPassed;
	}

	this.getThisTestMethodsNumberOfAssertionsFailed = function() {
		return this.thisTestMethodsAssertionsFailed;
	}

	this.getThisTestMethodsNumberOfAssertions = function() {
		return this.getThisTestMethodsNumberOfAssertionsFailed() + this.getThisTestMethodsNumberOfAssertionsPassed();
	}

	this.getTotalNumberOfTestMethodsPassed = function() {
		return this.totalTestMethodsPassed;
	}
	
	this.getTotalNumberOfTestMethodsFailed = function() {
		return this.totalTestMethodsFailed;
	}
	
	this.getTotalNumberOfTestMethods = function() {
		return this.getTotalNumberOfTestMethodsFailed() + this.getTotalNumberOfTestMethodsPassed();
	}

	this.getCurrentTestCase = function() {
		return this.currentTestCase;
	}
	
	this.getCurrentTestMethod = function() {
		return this.currentTestMethod;
	}

	this.addInterestedParty = function( party ) {
		party.setTracker( this );
		this.notifier.addInterestedParty( party );
	}
	
	this.reportTestSuiteStart = function() {
		this.notifier.notifyTestSuiteStart();
	}
	
	this.reportTestSuiteEnd = function() {
		this.notifier.notifyTestSuiteEnd();
	}

	this.reportTestCaseStart = function( testCase ) {
		this.thisTestCasePassed		= true;
		this.currentTestCase	 	= testCase;

		this.thisTestCasesAssertionsPassed	= 0;
		this.thisTestCasesAssertionsFailed	= 0;
		
		this.notifier.notifyTestCaseStart();
	}
	
	this.reportTestMethodStart = function( testMethod ) {
		this.thisTestMethodPassed		= true;
		this.currentTestMethod			= testMethod;

		this.thisTestMethodsAssertionsPassed	= 0;
		this.thisTestMethodsAssertionsFailed	= 0;

		this.notifier.notifyTestMethodStart();
	}
	
	this.reportTestMethodEnd = function( testMethod ) {

		if ( this.thisTestMethodPassed ) {
			this.totalTestMethodsPassed ++;
		} else {
			this.totalTestMethodsFailed ++;
		}

		this.currentTestMethod = '';

		this.notifier.notifyTestMethodEnd();
	}
	
	this.reportTestCaseEnd = function() {

		if ( this.thisTestCasePassed ) {
			this.totalTestCasesPassed ++;
		} else {
			this.totalTestCasesFailed ++;
		}
		
		this.currentTestCase = '';
		
		this.notifier.notifyTestCaseEnd();
	}
	
	this.reportAssertionPass = function( expectation ) {
		this.totalAssertionsPassed ++;
		this.thisTestCasesAssertionsPassed ++;
		this.thisTestMethodsAssertionsPassed ++;

		this.notifier.notifyAssertionPass( expectation );
	}
	
	this.reportAssertionFail = function( expectation ) {
		this.totalAssertionsFailed ++;
		this.thisTestCasesAssertionsFailed ++;
		this.thisTestMethodsAssertionsFailed ++;

		this.thisTestCasePassed		= false;
		this.thisTestMethodPassed	= false;

		this.notifier.notifyAssertionFail( expectation );
	}
}
